﻿//#define USE_FTS /// this is only helpful for example searches ... it's improves the overall population maybe 20% ?  Seems not worth it.
using System;
using System.Data;
using System.Data.SQLite;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows.Data;
using System.Windows;
using System.Xml;
using DataVirtualization;
using RadicalLookup = System.Collections.Generic.Dictionary<string, string>;

namespace JBrute
{
   // 'using' syntax for a C# 'typedef'
   using ResultsItemList_t = DataVirtualization.AsyncVirtualizingCollection<JBrute.ResultsItem>;
   using HistoryList_t = ObservableCollection<ResultsItem>;
   /*
    * Exposes interfaces to which we bind
   */
   public class ViewModel : INotifyPropertyChanged
   {
      #region INotifyPropertyChanged
      public event PropertyChangedEventHandler PropertyChanged;
      protected void Notify(string propName)
      {
         if (this.PropertyChanged != null)
         {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
         }
      }
      #endregion
      private int temp_1 = 0;
      private int temp_2 = 0;
      public HTMLHelper htmlhelper { get; private set; }
      public RadicalLookup m_krad = new RadicalLookup();
      public String AppDictPath { get; private set; }
      private ResultsItemProvider rip;
      private String sqlEDict;
      private String sqlExamples;
      private ResultsItemList_t _ResultsItemList;
      public ResultsItemList_t ResultsItemList
      {
         get { return _ResultsItemList; }
         set { _ResultsItemList = value; Notify("ResultsItemList"); }
      }

      private HistoryList_t _HistoryList;
      public HistoryList_t HistoryList
      {
         get { return _HistoryList; }
         set { _HistoryList = value; Notify("HistoryList"); }
      }
      private int _CurrentHistoryIndex;
      public int CurrentHistoryIndex
      {
         get { return _CurrentHistoryIndex; }
         set { _CurrentHistoryIndex = value; Notify("CurrentHistoryIndex"); }
      }
      public String Kana { get; private set; }
      public String Kanji { get; private set; }
      public String English { get; private set; }
      public String Grammar { get; private set; }

      public enum IndexType { EDict, KDict, ExDict };
      public enum SearchType { Lookup, VocabList };
      private String _MemUsage;
      public String MemUsage
      {
         get { return _MemUsage; }
         set { _MemUsage = value; Notify("MemUsage"); }
      }
      private String _DebugString;
      public String DebugString
      {
          get { return _DebugString; }
          set { _DebugString = value; Notify("DebugString"); }
      }
      private String _NumResultsEntries;
      public String NumResultsEntries
      {
         get { return _NumResultsEntries; }
         set { _NumResultsEntries = value; Notify("NumResultsEntries"); }
      }
      private String _NumResultsExamples;
      public String NumResultsExamples
      {
         get { return _NumResultsExamples; }
         set { _NumResultsExamples = value; Notify("NumResultsExamples"); }
      }
      private String _SearchTime;
      public String SearchTime
      {
         get { return _SearchTime; }
         set { _SearchTime = value; Notify("SearchTime"); }
      }
      private bool _ClipBoardMonitor;
      public bool ClipBoardMonitor
      {
         get { return _ClipBoardMonitor; }
         set { _ClipBoardMonitor = value; Notify("ClipBoardMonitor"); }
      }
      private bool _PinToTop;
      public bool PinToTop
      {
         get { return _PinToTop; }
         set { _PinToTop = value; Notify("PinToTop"); }
      }
      public enum IMEModeType { Disabled, Off, Hirgana, Katakana, Kanji };
      private IMEModeType _IMEMode;
      public IMEModeType IMEMode
      {
         get { return _IMEMode; }
         set { _IMEMode = value; Notify("IMEMode"); }
      }

      public static ViewModel Instance = new ViewModel();
      private ArrayList HitsInOrder;

      public ViewModel()
      {
         DebugString = "ViewModel (CTOR)";
         sqlEDict = sqlExamples = null;
         String AppPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
         AppDictPath = AppPath + "\\..\\data";
         String kradfile = AppDictPath + "\\kradfile-u";
         ResultsItemProvider.connString = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" + AppDictPath + "\\JBrute.db";

         using (StreamReader sr = new StreamReader(kradfile))
         {
             String line;
             while ((line = sr.ReadLine()) != null)
             {
                 string[] parts = line.Split(':');
                 if (parts.Length == 2 && parts[0].Length == 2)
                     m_krad.Add(parts[0].Trim(), parts[1]);
             }
         }
         htmlhelper = new HTMLHelper();
         CurrentHistoryIndex = 0;
         HistoryList = new HistoryList_t();
         IMEMode = IMEModeType.Disabled;
         ClipBoardMonitor = true;

         // fire off a BG thread that primes SQLite.  Otherwise first query takes a long time!
         sqlEDict = "SELECT sense.fk FROM  \r\n                        sense INNER JOIN gloss on gloss.fk = sense.id WHERE (gloss.value LIKE 'book%') group by sense.fk  ORDER BY gloss.value ASC ;";
         sqlExamples = "SELECT id from examples WHERE (examples.english LIKE '%book%') ;";
         rip = new ResultsItemProvider("book", sqlEDict, sqlExamples);
         ResultsItemList = new AsyncVirtualizingCollection<ResultsItem>(rip, 10 /*page size*/, 60 * 1000 /*timeout*/) as ResultsItemList_t;
         ((MainWindow)Application.Current.MainWindow).DataContext = ResultsItemList;
         ((MainWindow)Application.Current.MainWindow).DataContext = null;
         DebugString = String.Empty;
      }

      public static bool isKanji(string s)
      {
         bool bIsKanji = false;
         foreach (Char c in s)
         {
            if (isKanji(c))
            {
               bIsKanji = true;
               break;
            }
         }
         return bIsKanji;
      }
      public static bool isKana(string s)
      {
         bool bIsKana = false;
         foreach (Char c in s)
         {
            if (isKana(c))
            {
               bIsKana = true;
               break;
            }
         }
         return bIsKana;
      }
      public static bool isKanji(char c)
      {
         return ((0x4E00 <= c) && (c <= 0xFA2D));
      }
      public static bool isKana(char c)
      {
         return ((0x3041 <= c) && (c <= 0x30FE));
      }
      private enum ParseState { Init, Quoted, Unquoted, QuotedCompleted, EOF } ;
      private String getSatisfiedPhraseQuery(SQLiteConnection conn, String checkme, ref ArrayList tempHits)
      {
         if (checkme.Length < 2)
            return null;
         String where = null;
         SQLiteCommand sqCommand = null;
         if (isKanji(checkme))
         {
            where = "k_ele.value ='" + checkme + "'";
            sqCommand = new SQLiteCommand("SELECT count(1) from k_ele WHERE " + where + ";", conn);
            long count = (long) sqCommand.ExecuteScalar();
            temp_2++;

            if (count == 1)
            {
               sqCommand = new SQLiteCommand("SELECT fk from k_ele WHERE " + where + ";", conn);
               long s = (long)sqCommand.ExecuteScalar();
               temp_2++;
               tempHits.Add(s);
               return where;
            }
            where = "k_ele.value >='" + checkme + "' AND k_ele.value < '" + checkme + "龠';";
            sqCommand = new SQLiteCommand("SELECT count(1) from k_ele WHERE " + where + ";", conn);
            count = (long)sqCommand.ExecuteScalar();
            temp_2++;
            if (count == 1)
            {
               sqCommand = new SQLiteCommand("SELECT fk from k_ele WHERE " + where + ";", conn);
               long s = (long)sqCommand.ExecuteScalar();
               temp_2++;
               tempHits.Add(s);
               return where;
            }
         }
         if (checkme.Length > 1)
         {  // Want things like　ページ or でも but not things like に、は
            where = "r_ele.value ='" + checkme + "'";
            sqCommand = new SQLiteCommand("SELECT count(1) from r_ele  WHERE " + where + ";", conn);
            long count = (long)sqCommand.ExecuteScalar();
            temp_2++;
            if (count == 1)
            {
               sqCommand = new SQLiteCommand("SELECT fk from r_ele WHERE " + where + ";", conn);
               long s = (long)sqCommand.ExecuteScalar();
               temp_2++;
               tempHits.Add(s);
               return where;
            }
            where = "r_ele.value >='" + checkme + "' AND r_ele.value < '" + checkme + "龠';";
            sqCommand = new SQLiteCommand("SELECT count(1) from r_ele  WHERE " + where + ";", conn);
            count = (long)sqCommand.ExecuteScalar();
            temp_2++;
            if (count == 1)
            {
               sqCommand = new SQLiteCommand("SELECT fk from r_ele WHERE " + where + ";", conn);
               long s = (long)sqCommand.ExecuteScalar();
               temp_2++;
               tempHits.Add(s);
               return where;
            }
         }
         return null;
      }
      Char getFirstNonWildcardChar(String s)
      {
         Char c = 'A';
         int i;
         for (i = 0; i < s.Length; i++)
         {
            c = s[i];
            if (c != '%' && c != '_')
               break;
         }
         if (i == s.Length)
            throw new InvalidOperationException("invalid input: " + s);
         return c;
      }
      String WhereEntriesMatch(ArrayList qtermKana, ArrayList qtermKanji, ArrayList qtermE)
      {
         bool ForcePrefix = (qtermE.Count + qtermKana.Count + qtermKanji.Count == 1);
         String sqlWhere = "WHERE ";
         for (int i = 0; i < qtermKana.Count; i++)
         {
            String s = qtermKana[i] as String;
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(r_ele.value LIKE '" + s + "') ";
            else if (ForcePrefix)
               sqlWhere += "(r_ele.value LIKE '" + s + "%') ";
            else
               sqlWhere += "(r_ele.value = '" + s + "') ";

            if (i < qtermKana.Count - 1 || qtermKanji.Count > 0 || qtermE.Count > 0)
               sqlWhere += "AND ";
         }
         for (int i = 0; i < qtermKanji.Count; i++)
         {
            String s = qtermKanji[i] as String;
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(k_ele.value LIKE '" + s + "') ";
            else if (ForcePrefix)
               sqlWhere += "(k_ele.value LIKE '" + s + "%') ";
            else
               sqlWhere += "(k_ele.value = '" + s + "') ";

            if (i < qtermKanji.Count - 1 || qtermE.Count > 0)
               sqlWhere += "AND ";
         }
         for (int i = 0; i < qtermE.Count; i++)
         {
            String s = qtermE[i] as String;
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(gloss.value LIKE '" + s + "') ";
            else if (ForcePrefix)
               sqlWhere += "(gloss.value LIKE '" + s + "%') ";
            else // multiple english terms need to search throughout gloss (so that "to be hungry" works)
               //sqlWhere += "(gloss.value = '" + s + "') ";
               sqlWhere += "(gloss.value LIKE '%" + s + "%') ";
            if (i < qtermE.Count - 1)
               sqlWhere += "AND ";

         }
         return sqlWhere;
      }
      String WhereExamplesMatch(ArrayList qtermKana, ArrayList qtermKanji, ArrayList qtermE)
      {
         String sqlWhere = "WHERE ";
         for (int i = 0; i < qtermKana.Count; i++)
         {
            String s = qtermKana[i] as String;
#if USE_FTS
            sqlWhere += "(examples_fts.japanese MATCH '" + s + "') ";
#else
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(examples.japanese LIKE '" + s + "') ";
            else
               sqlWhere += "(examples.japanese LIKE '%" + s + "%') ";
#endif
            if (i < qtermKana.Count - 1 || qtermKanji.Count > 0 || qtermE.Count > 0)
               sqlWhere += "AND ";
         }
         for (int i = 0; i < qtermKanji.Count; i++)
         {
            String s = qtermKanji[i] as String;
#if USE_FTS
               sqlWhere += "(examples_fts.japanese MATCH '" + s + "') ";
#else
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(examples.japanese LIKE '" + s + "') ";
            else
               sqlWhere += "(examples.japanese LIKE '%" + s + "%') ";
#endif
            if (i < qtermKanji.Count - 1 || qtermE.Count > 0)
               sqlWhere += "AND ";
         }
         for (int i = 0; i < qtermE.Count; i++)
         {
            String s = qtermE[i] as String;
#if USE_FTS
               sqlWhere += "(examples_fts.english MATCH '" + s + "') ";
#else
            if (s.Contains('%') || s.Contains('_'))
               sqlWhere += "(examples.english LIKE '" + s + "') ";
            else
               sqlWhere += "(examples.english LIKE '%" + s + "%') ";
#endif
            if (i < qtermE.Count - 1)
               sqlWhere += "AND ";
         }
         return sqlWhere;
      }
      // Sets up the SQL statements so that the we can pass them to the RIP for DB access
      // - Lookup, only one term:  Book% (keystroke, dictionary only)
      // - Lookup, multi-Term:
      //    - terms can have wildcards:  _気
      //    - terms can be quoted phrase: "Can I help you"
      //    - terms can mix J and E
      //    - terms are separated by white space
      // - VocabList

      public void SearchUtil(String qterm, SearchType searchType)
      {
         DebugString = "SearchUtil";
         String sort = null;
         qterm = qterm.Replace("　", " ");// space
         qterm = qterm.Replace("＿", "_");// wildcard "?"
         qterm = qterm.Replace("％", "%");// wildcard "*"
         qterm = qterm.Replace("”", "\"");// quote
         qterm = qterm.Trim();
         sqlEDict = string.Empty;
         sqlExamples = String.Empty;
         if (String.IsNullOrEmpty(qterm) || qterm.FirstOrDefault(p => p != '%' && p != '_') == '\0')
         {
            ((MainWindow)Application.Current.MainWindow).DataContext = null;
            ((MainWindow)Application.Current.MainWindow).lastClipboardCopy = String.Empty;
            ViewModel.Instance.SearchTime = String.Empty;
            ViewModel.Instance.NumResultsEntries = String.Empty;
            ViewModel.Instance.NumResultsExamples = String.Empty;
            return;
         }
         String ReqdForSortTable = String.Empty;
         if (searchType != SearchType.VocabList)
         {
            if (Application.Current.Dispatcher.CheckAccess())
               sort = ((MainWindow)(System.Windows.Application.Current.MainWindow)).getSort(ref ReqdForSortTable);
            else
            {
               Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)delegate()
               {
                  sort = ((MainWindow)(System.Windows.Application.Current.MainWindow)).getSort(ref ReqdForSortTable);
               });
            }
         }
         // Generic Search:  WS delimits terms, all of which must be present.  Supports embedded wild cards.
         if (searchType == SearchType.Lookup)
         {
            // Break qterm into J and E parts using a state machine (Transitions and Actions)
            char[] seps = { ' ' };
            String[] parts = qterm.Split(seps);
            ArrayList qtermKanji = new ArrayList();
            ArrayList qtermKana = new ArrayList();
            ArrayList qtermE = new ArrayList();
            String sQuoted = String.Empty;
            ParseState eState = ParseState.Init;
            foreach (String s in parts)
            {
               // Transitions
               if ((eState == ParseState.Init) || (eState == ParseState.Unquoted))
               {
                  if ((s[0] == '\"') && (!s.EndsWith("\"")))
                     eState = ParseState.Quoted;
                  else
                     eState = ParseState.Unquoted;
               }
               else if (eState == ParseState.Quoted)
               {
                  if (s.EndsWith("\""))
                     eState = ParseState.QuotedCompleted;
               }

               // Actions
               if (eState == ParseState.Quoted)
               {
                  sQuoted += " " + s;
               }
               else if (eState == ParseState.Unquoted)
               {  // "cannot use a * or ? symbol as the first character of a search.
                  if (isKanji(s))
                     qtermKanji.Add(s);
                  else if (isKana(s))
                     qtermKana.Add(s);
                  else
                     qtermE.Add(s);

               }
               else if (eState == ParseState.QuotedCompleted)
               {
                  sQuoted += " " + s;
                  if (!sQuoted.StartsWith("\"") || !sQuoted.EndsWith("\""))
                  {
                     sQuoted = sQuoted.TrimStart(' ', '\"');
                     sQuoted = sQuoted.TrimEnd(' ', '\"');
                  }
                  else
                     throw new InvalidOperationException("Invalid parsing of quoted string");
                  char c = getFirstNonWildcardChar(sQuoted);
                  if (isKanji(sQuoted))
                     qtermKanji.Add(sQuoted);
                  else if (isKana(sQuoted))
                     qtermKana.Add(sQuoted);
                  else
                     qtermE.Add(sQuoted);
                  eState = ParseState.Unquoted;
               }
            }
            if (!((eState == ParseState.Unquoted) || (eState == ParseState.QuotedCompleted)))
               return;  // MessageDialog.Show("Malformed query term.", null);

            // If there is a single term and it doesn't end in a Wildcard, make it a suffix query (normal key-by-key lookup)
            // msr:  "ruins" should list all the entries "ruins%" but should also get examples like "The Stone Age ruins..."
            //  that is, the same modifcation for both entries and examples is wrong
            String sqlWhere = WhereEntriesMatch(qtermKana, qtermKanji, qtermE);
            if (qtermKana.Count > 0)
            {
               sqlEDict = @"SELECT r_ele.fk FROM  
                        r_ele ";
               if ((ReqdForSortTable == "k_ele") || (qtermKanji.Count > 0))
                  sqlEDict += "LEFT OUTER JOIN k_ele on k_ele.fk = r_ele.fk ";
               else if ((ReqdForSortTable == "gloss") || (qtermE.Count > 0))
                  sqlEDict += @"
                              INNER JOIN sense on sense.fk = r_ele.fk 
                              INNER JOIN gloss on gloss.fk = sense.id ";
               sqlEDict += sqlWhere + "group by r_ele.fk";
            }
            else if (qtermKanji.Count > 0)
            {
               sqlEDict = @"SELECT k_ele.fk FROM  
                        k_ele ";
               if ((ReqdForSortTable == "r_ele") || (qtermKana.Count > 0))
                  sqlEDict += "INNER JOIN r_ele on r_ele.fk = k_ele.fk ";
               else if ((ReqdForSortTable == "gloss") || (qtermE.Count > 0))
                  sqlEDict += @"
                              INNER JOIN sense on sense.fk = k_ele.fk  
                              INNER JOIN gloss on gloss.fk = sense.id ";
               sqlEDict += sqlWhere + "group by k_ele.fk ";
            }
            else if (qtermE.Count > 0)
            {
               sqlEDict = @"SELECT sense.fk FROM  
                        sense INNER JOIN gloss on gloss.fk = sense.id ";
               if ((ReqdForSortTable == "r_ele") || (qtermKana.Count > 0))
                  sqlEDict += "INNER JOIN r_ele on r_ele.fk = sense.fk ";
               else if ((ReqdForSortTable == "k_ele") || (qtermKanji.Count > 0))
                  sqlEDict += "LEFT OUTER JOIN k_ele on k_ele.fk = sense.fk ";
               sqlEDict += sqlWhere + "group by sense.fk ";

            }
            sqlWhere = WhereExamplesMatch(qtermKana, qtermKanji, qtermE);
#if USE_FTS
            sqlExamples = "SELECT id from examples_fts " + sqlWhere;
#else
            sqlExamples = "SELECT id from examples " + sqlWhere;
#endif
         }
         else if (searchType == SearchType.VocabList)
         {
            temp_1 = temp_2 = 0;
            DateTime TimeStart = DateTime.Now;
            HitsInOrder = new ArrayList();
            sqlExamples = String.Empty;
            //// Create a boolean query which gets every recognizeable term in the qterm.
            bool bUseKanjiIndex = true;
            ArrayList qBool = new ArrayList();
            String substring = String.Empty;
            int iStartPos = 0;
            int len = 1;
            ArrayList tempHits = new ArrayList();
            SQLiteConnection conn = new SQLiteConnection(ResultsItemProvider.connString);
            conn.Open();
            String where;
            SQLiteCommand sqCommand;
            //// Move through qterm and form the maximum substring which satisfies
            //// a Prefix Query using either the Kanji or the Kana index.
            //// - if that substring satisifies a Phrase (exact match) query, then add that Phrase Query
            //// - otherwise, form sub-substrings by removing Kana from the end 
            //// -   until we do satisfy a Phrase query.  Add that  Phrase Query.
            //// - if no such sub-substring satisfies a Phrase query, then try the original Prefix Query 
            //// -   if it has exactly one return, then add that Prefix Query
            while (iStartPos < qterm.Length)
            {
               for (len = 1; len <= qterm.Length - iStartPos; len++)
               {  // This loop finds the Maximal PrefixQuery Term
                  tempHits.Clear();
                  substring = qterm.Substring(iStartPos, len);
                  DebugString = "Seeking Maximal PrefixQuery Term:  "+ substring;
                  // skip over punctuation b/c it causes needless hits on the db.
                  if (!isKana(substring[len - 1]) && !isKanji(substring[len - 1]))
                     break;
#if USE_FTS
                  where = "WHERE k_ele_fts.value MATCH '" + substring + "';";
                  sqCommand = new SQLiteCommand("SELECT count(1) from k_ele_fts "+ where, conn);
#else
                  where = "WHERE k_ele.value >='" + substring + "' AND k_ele.value < '" + substring + "龠';";
                  sqCommand = new SQLiteCommand("SELECT count(1) from k_ele "+ where, conn);
#endif
                  long count = (long) sqCommand.ExecuteScalar();
                  temp_1++;
                  if (count > 0)
                  {
                     bUseKanjiIndex = true;
                     continue;
                  }
                  else
                  {
#if USE_FTS
                     where = "WHERE r_ele_fts.value MATCH '" + substring + "';";
                     sqCommand = new SQLiteCommand("SELECT count(1) from r_ele_fts "+ where, conn);
#else
                     where = "WHERE r_ele.value >='" + substring + "' AND r_ele.value < '" + substring + "龠';";
                     sqCommand = new SQLiteCommand("SELECT count(1) from r_ele " + where, conn);
#endif
                     count = (long)sqCommand.ExecuteScalar();
                     temp_1++;
                     if (count > 0)
                     {
                        bUseKanjiIndex = false;
                        continue;
                     }
                     break; // exit loop here when no more prefix queries work.
                  }
               }
               // This is the Maximal PrefixQuery Term
               substring = qterm.Substring(iStartPos, len - 1); // back off one char so that there were some prefix matches
               DebugString = "Got Maximal PrefixQuery Term:  " + substring;
               if ((substring.Length > 0) && (isKanji(substring[0]) || isKana(substring[0])))
               {
                  String phraseQ = getSatisfiedPhraseQuery(conn, substring, ref tempHits);
                  while (phraseQ == null && substring.Length > 1 && isKana(substring[substring.Length - 1]))
                  {
                     substring = substring.Substring(0, substring.Length - 1);
                     phraseQ = getSatisfiedPhraseQuery(conn, substring, ref tempHits);
                  }
                  if (phraseQ != null)
                  {
                     qBool.Add(phraseQ);
                     HitsInOrder.AddRange(tempHits);
                     iStartPos += substring.Length;
                  }
                  else
                     iStartPos++;
               }
               else
                  iStartPos++;
            }
            conn.Close();
         }
         if (searchType != SearchType.VocabList)
         {
            if (!String.IsNullOrEmpty(sqlEDict))
               sqlEDict += " " + sort + ";";
            if (!String.IsNullOrEmpty(sqlExamples))
               sqlExamples += ";";
         }
         else
         {
            sort = " ORDER BY CASE fk ";
            for (int i = 0; i < HitsInOrder.Count; i++)
            {
               String s = String.Format("WHEN {0} THEN {1} ", HitsInOrder[i], i);
               sort += s;
            }
            sort += "END;";

         }
         UpdateResultsBinding(qterm);

         String msg = String.Format("{0} MB", System.Diagnostics.Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024));
         MemUsage = msg;
         DebugString = String.Format("DB hits to find the MPT:  {0}, DB hits to get query terms {1}", temp_1, temp_2); // String.Empty;
      }

      public void UpdateResultsBinding(string qterm)
      {
         System.Diagnostics.Trace.WriteLine("UpdateResultsBinding...:  " + qterm);
         if (rip != null)
            rip.IsNoLongerNeeded = true;
         if (!String.IsNullOrEmpty(sqlEDict) && !String.IsNullOrEmpty(sqlExamples))
            rip = new ResultsItemProvider(qterm, sqlEDict, sqlExamples);
         else
            rip = new ResultsItemProvider(ref HitsInOrder);
         ResultsItemList = new AsyncVirtualizingCollection<ResultsItem>(rip, 10 /*page size*/, 60 * 1000 /*timeout*/) as ResultsItemList_t;
         if (Application.Current.Dispatcher.CheckAccess())
         {
            ((MainWindow)Application.Current.MainWindow).DataContext = ResultsItemList;
         }
         else
         {
            Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)delegate()
            {
               ((MainWindow)Application.Current.MainWindow).DataContext = ResultsItemList;
            });
         }
         System.Diagnostics.Trace.WriteLine("...UpdateResultsBinding:  " + qterm);

      }

      public class HTMLHelper
      {

         private String m_xsl;
         private System.Windows.Forms.WebBrowser m_webBrowser;
         public HTMLHelper()
         {
            TextReader tr = new StreamReader(System.Windows.Forms.Application.StartupPath + "\\..\\data\\JBrute.xsl");
            m_xsl = tr.ReadToEnd();
            m_webBrowser = ((MainWindow)(Application.Current.MainWindow)).details;
         }
         private void populate_examples(ref ResultsItem ri, SQLiteConnection conn)
         {
            int count = 0;
            ri.examples = new ArrayList();
            foreach (String s in ri.kanji)
            {
#if USE_FTS
               String sql = "SELECT examples.id from examples WHERE examples_fts.japanese MATCH '" + s + "';";
#else
               String sql = "SELECT examples.id from examples WHERE examples.japanese LIKE '%" + s + "%';";
#endif
               SQLiteCommand sqCommand = new SQLiteCommand(sql, conn);
               SQLiteDataReader sqReader = sqCommand.ExecuteReader();
               while (sqReader.Read())
               {
                  if (count++ > 2)
                     return;
                  long l = sqReader.GetInt64(0);
                  ri.examples.Add(l);
               }
            }
         }

         public void Display(ResultsItem ri, bool AddToHistory)
         {
            ViewModel.Instance.DebugString = String.Format("Display");
            XmlWriterSettings writerSettings = new XmlWriterSettings();
            writerSettings.OmitXmlDeclaration = true; // omits UCS-16 declaration which complicates viewing this document
            writerSettings.Indent = true;

            StringBuilder sbuilder = new StringBuilder();
            XmlWriter xmlWriter = XmlWriter.Create(sbuilder, writerSettings);
            String PItext = "type=\"text/xsl\" href=\"../JBrute.xsl\"";
            xmlWriter.WriteProcessingInstruction("xml-stylesheet", PItext);

            xmlWriter.WriteStartElement("root");
            if (ri.kanji != null && ri.kanji.Count > 0)
            {
               foreach (String s in ri.kanji)
               {
                  xmlWriter.WriteStartElement("kanji");
                  xmlWriter.WriteString(s);
                  xmlWriter.WriteEndElement();
               }
            }
            if (ri.kana != null && ri.kana.Count > 0)
            {
               foreach (String s in ri.kana)
               {
                  xmlWriter.WriteStartElement("kana");
                  xmlWriter.WriteString(s);
                  xmlWriter.WriteEndElement();
               }
            }
            SQLiteConnection conn = new SQLiteConnection(ResultsItemProvider.connString);
            conn.Open();
            //String sql = "Select entity.entity || ' - ' || entity.expansion from entity INNER JOIN pos on pos.entity = entity.id WHERE pos.fk = " + ri.id + ";";
            String sql = "Select entity.entity from entity INNER JOIN pos on pos.entity = entity.id WHERE pos.fk = " + ri.id + ";";
            SQLiteCommand sqCommand = new SQLiteCommand(sql, conn);
            SQLiteDataReader sqReader = sqCommand.ExecuteReader();
            while (sqReader.Read())
            {
               String s = sqReader.GetString(0);
               ri.geninfo += s + " / ";
            }
            if (!String.IsNullOrEmpty(ri.geninfo))
               ri.geninfo = ri.geninfo.TrimEnd(' ', '/');
            if (ri.geninfo != null && ri.geninfo.Length > 0)
            {
               xmlWriter.WriteStartElement("pos");
               xmlWriter.WriteString(ri.geninfo);
               xmlWriter.WriteEndElement();
            }
            if (ri.gloss != null)
            {
               foreach (String s in ri.gloss)
               {
                  xmlWriter.WriteStartElement("gloss");
                  xmlWriter.WriteString(s);
                  xmlWriter.WriteEndElement();
               }
            }
            populate_examples(ref ri, conn);
            if (ri.examples != null && ri.examples.Count > 0)
            {
               // Show up to three example usages
               String format = "{0} / {1}\n";
               for (int i = 0; i < 3 && i < ri.examples.Count; i++)
               {
                  sql = "SELECT english, japanese from examples WHERE examples.id = " + ri.examples[i] + ";";
                  sqCommand = new SQLiteCommand(sql, conn);
                  sqReader = sqCommand.ExecuteReader() as SQLiteDataReader;
                  if (!sqReader.Read())
                     break;
                  String t = String.Format(format, sqReader.GetString(sqReader.GetOrdinal("english")), sqReader.GetString(sqReader.GetOrdinal("japanese")));
                  xmlWriter.WriteStartElement("usage");
                  xmlWriter.WriteString(t);
                  xmlWriter.WriteEndElement();
               }
            }
            if (ri.kanji != null && ri.kanji.Count > 0)
            {
                  // Print out Kanji Details, in the order they appear in the entry
                  foreach (String s in ri.kanji)
                  {
                     for (int i = 0; i < s.Length; i++)
                     {
                        Char W = s[i];
                        if (isKanji(W) && s.IndexOf(W) == i)
                        {
                           xmlWriter.WriteStartElement("kanjidetail");
                           xmlWriter.WriteStartElement("moji");
                           xmlWriter.WriteString(W.ToString());
                           xmlWriter.WriteEndElement();
                           foreach (char c in ViewModel.Instance.m_krad[W.ToString()])
                           {
                              if (RadicalTables.Rad.ContainsKey(c.ToString()))
                              {
                                 xmlWriter.WriteStartElement("radical");
                                 xmlWriter.WriteString(c.ToString() + ": " + RadicalTables.Rad[c.ToString()]);
                                 xmlWriter.WriteEndElement();
                              }
                           }
                           xmlWriter.WriteStartElement("post_moji");
                           sql = "Select group_concat(count, '/') from stroke_count INNER JOIN character on character.id = stroke_count.fk WHERE character.literal = '" + W + "' GROUP BY character.id";
                           sqCommand = new SQLiteCommand(sql, conn);
                           String stroke_count = sqCommand.ExecuteScalar().ToString();
                           sql = "Select group_concat(value, '/') from reading INNER JOIN character on character.id = reading.fk WHERE character.literal = '" + W + "' and reading.type in ('ja_on', 'ja_kun') GROUP BY character.id";
                           sqCommand = new SQLiteCommand(sql, conn);
                           String yomikata = sqCommand.ExecuteScalar().ToString();
                           sql = "Select group_concat(value, '/') from meaning INNER JOIN character on character.id = meaning.fk WHERE character.literal = '" + W + "' and meaning.lang = 'en' GROUP BY character.id";
                           sqCommand = new SQLiteCommand(sql, conn);
                           String meaning = sqCommand.ExecuteScalar().ToString();
                           xmlWriter.WriteString("(" + stroke_count + ") [" + yomikata + "] - " + meaning);
                           xmlWriter.WriteEndElement();
                           xmlWriter.WriteEndElement();
                        }
                     }
                  }
            }
            conn.Close();

            xmlWriter.Close();
            // We need to transform the XML for the WebBrowser.  There's no way to
            // to make it do the transform unless you navigate from a file or a URL
            String xmlString = sbuilder.ToString();
            MemoryStream memStream;
            //' Create a xml-document from the sent-in xml-string
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xmlString);

            //' Load the xsl into another document
            XmlDocument xslDoc = new XmlDocument();
            xslDoc.LoadXml(m_xsl);

            //' Create a transformation
            System.Xml.Xsl.XslCompiledTransform trans = new System.Xml.Xsl.XslCompiledTransform();
            trans.Load(xslDoc);

            //' Create a memory stream for output
            memStream = new MemoryStream();

            //' Do the transformation according to the XSLT and save the result in our memory stream
            trans.Transform(xmlDoc, null, memStream);
            memStream.Position = 0;

            // Debugging aid:  serialize the XML to a file (don't forget to manually adjust location of the xsl file.)
            //String filepath = ViewModel.Instance.AppDictPath + "\\jbrute_sample.xml";
            //TextWriter tw = new StreamWriter(filepath);
            //tw.WriteLine(xmlString);
            //tw.Close();

            // Navigate to the transformed HTML page
            ((MainWindow)(Application.Current.MainWindow)).details.DocumentStream = memStream;

            if (AddToHistory)
            {
               while (ViewModel.Instance.CurrentHistoryIndex < ViewModel.Instance.HistoryList.Count - 1)
                  ViewModel.Instance.HistoryList.RemoveAt(ViewModel.Instance.HistoryList.Count - 1);
               ViewModel.Instance.HistoryList.Add(ri);
               ViewModel.Instance.CurrentHistoryIndex = ViewModel.Instance.HistoryList.Count - 1;
            }
            //ViewModel.Instance.DebugString = String.Format("");
         }
      }
   }

   public class ArrayListConverter : IValueConverter
   {
      public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         String retval = String.Empty;
         ArrayList list = value as ArrayList;
         if (list != null && list.Count > 0)
            foreach (String s in list)
               retval += s + " / ";
         retval = retval.TrimEnd(' ', '/');
         return retval;
      }

      public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         throw new NotImplementedException();
      }
   }
   // We can not bind HTML Stream directly so we bind an unused (Tag) DependencyObject
   // to the selected ResultsItem and manually update the HTML Stream based on that.
   public class DetailsConverter : IValueConverter
   {
      public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         ResultsItem ri = value as ResultsItem;
         if (ri != null)
            ViewModel.Instance.htmlhelper.Display(ri, true);
         return null;
      }

      public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         throw new NotImplementedException();
      }
   }
   public class IMEModeConverter : IValueConverter
   {
      public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         ViewModel.IMEModeType imemode = (ViewModel.IMEModeType)value;
         String retval = String.Empty;
         switch (ViewModel.Instance.IMEMode)
         {
            case ViewModel.IMEModeType.Hirgana:
               retval = "Lookup Text - ひらがな (Hiragana)";
               break;
            case ViewModel.IMEModeType.Katakana:
               retval = "Lookup Text - カタカナ (Katakana)";
               break;
            case ViewModel.IMEModeType.Kanji:
               retval = "Lookup Text - 漢字 (Kanji)";
               break;
            case ViewModel.IMEModeType.Off:
               retval = "Lookup Text - (IME off)";
               break;
            case ViewModel.IMEModeType.Disabled:
               retval = "Lookup Text - (IME off ... not installed!)";
               break;
         }
         return retval;
      }

      public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         throw new NotImplementedException();
      }
   }
   /// <summary>
   /// Calculates the column width required to fill the view in a GridView
   /// For usage examples, see http://leghumped.com/blog/2009/03/11/wpf-gridview-column-width-calculator/
   /// </summary>
   public class WidthConverter : IValueConverter
   {
      /// <summary>
      /// Converts the specified value.
      /// </summary>
      /// <param name="value">The parent Listview.</param>
      /// <param name="type">The type.</param>
      /// <param name="parameter">
      /// If no parameter is given, the remaning with will be returned.
      /// If the parameter is an integer acts as MinimumWidth, the remaining with will be returned only if it's greater than the parameter
      /// If the parameter is anything else, it's taken to be a percentage. Eg: 0.3* = 30%, 0.15* = 15%
      /// </param>
      /// <param name="culture">The culture.</param>
      /// <returns>The width, as calculated by the parameter given</returns>
      public object Convert(object value, Type type, object parameter, System.Globalization.CultureInfo culture)
      {
         double actualWidth = (double)value;
         if (Application.Current.MainWindow == null)
            return 42.0;
         if (!Application.Current.MainWindow.GetType().ToString().Contains("JBrute.MainWindow"))
            return 42.0;
         System.Windows.Controls.GridView grdView = ((MainWindow)(Application.Current.MainWindow)).lvResults.View as System.Windows.Controls.GridView;
         int minWidth = 0;
         bool widthIsPercentage = parameter != null && !int.TryParse(parameter.ToString(), out minWidth);
         if (widthIsPercentage)
         {
            string widthParam = parameter.ToString();
            double percentage = double.Parse(widthParam.Substring(0, widthParam.Length - 1));
            return actualWidth * percentage;
         }
         else
         {
            double total = 0;
            for (int i = 0; i < grdView.Columns.Count - 1; i++)
            {
               total += grdView.Columns[i].ActualWidth;
            }
            double remainingWidth = actualWidth - total;
            if (remainingWidth > minWidth)
            { // fill the remaining width in the ListView
               return remainingWidth;
            }
            else
            { // fill remaining space with MinWidth
               return minWidth;
            }
         }
      }

      public object ConvertBack(object o, Type type, object parameter, System.Globalization.CultureInfo culture)
      {
         throw new NotSupportedException();
      }
   }
   // Enable or disable "Back" and "Forward" buttons
   public class CurrentHistoryIndexConverter : IValueConverter
   {
      public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         bool retval = false;
         String param = parameter as String;
         int index = (int)value;
         if (param.Contains("back"))
            retval = (index > 0);
         else if (param.Contains("fwd"))
            retval = (index < ViewModel.Instance.HistoryList.Count - 1);
         return retval;
      }

      public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
         throw new NotImplementedException();
      }
   }
}
